package cn.scooper.com.whiteboard.views;

import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.SystemClock;
import android.support.annotation.NonNull;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.animation.AnimationUtils;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;

import com.rey.material.app.ThemeManager;
import com.rey.material.drawable.RippleDrawable;
import com.rey.material.util.ThemeUtil;
import com.rey.material.util.TypefaceUtil;
import com.rey.material.util.ViewUtil;
import com.rey.material.widget.RippleManager;
import com.scooper.cn.whiteboard.R;

import cn.scooper.com.easylib.utils.BitmapUtils;

/**
 * 颜色选择控件
 */
public class Slider extends View implements ThemeManager.OnThemeChangedListener {

    protected int mStyleId;
    protected int mCurrentStyle = ThemeManager.THEME_UNDEFINED;
    private RippleManager mRippleManager;
    private Paint mPaint;
    private RectF mDrawRect;
    private RectF mTempRect;
    private Path mMarkPath;

    private int mMinValue = 0;
    private int mMaxValue = 100;
    private int mStepValue = 1;

    private boolean mDiscreteMode = false;

    private int mTrackSize = -1;
    private int mThumbBorderSize = -1;
    private int mThumbRadius = -1;
    private int mThumbFocusRadius = -1;
    private float mThumbPosition = -1;
    private Typeface mTypeface = Typeface.DEFAULT;
    private int mTextSize = -1;
    private int mGravity = Gravity.CENTER;
    private int mTravelAnimationDuration = -1;
    private int mTransformAnimationDuration = -1;
    private Interpolator mInterpolator;
    private int mBaselineOffset;

    private int mTouchSlop;
    private PointF mMemoPoint;
    private boolean mIsDragging;
    private float mThumbCurrentRadius;
    private float mThumbFillPercent;
    private boolean mAlwaysFillThumb = false;

    private ThumbRadiusAnimator mThumbRadiusAnimator;
    private ThumbStrokeAnimator mThumbStrokeAnimator;
    private ThumbMoveAnimator mThumbMoveAnimator;

    private boolean mIsRtl = false;

    private Bitmap mBackground;
    private int mPickColor;
    private OnColorChangeListener mOnColorChangeListener;

    public Slider(Context context) {
        super(context);
        init(context, null, 0, 0);
    }

    public Slider(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs, 0, 0);
    }

    public Slider(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs, defStyleAttr, 0);
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public Slider(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init(context, attrs, defStyleAttr, defStyleRes);
    }

    public void setOnColorChangeListener(OnColorChangeListener listener) {
        this.mOnColorChangeListener = listener;
    }

    protected void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

        mDrawRect = new RectF();
        mTempRect = new RectF();

        mThumbRadiusAnimator = new ThumbRadiusAnimator();
        mThumbStrokeAnimator = new ThumbStrokeAnimator();
        mThumbMoveAnimator = new ThumbMoveAnimator();

        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
        mMemoPoint = new PointF();

        mBackground = BitmapFactory.decodeResource(getResources(), R.mipmap.color_pick_bg);

        applyStyle(context, attrs, defStyleAttr, defStyleRes);

        if (!isInEditMode())
            mStyleId = ThemeManager.getStyleId(context, attrs, defStyleAttr, defStyleRes);
    }

    public void applyStyle(int resId) {
        ViewUtil.applyStyle(this, resId);
        applyStyle(getContext(), null, 0, resId);
    }

    protected void applyStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        getRippleManager().onCreate(this, context, attrs, defStyleAttr, defStyleRes);

        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Slider, defStyleAttr, defStyleRes);
        int minValue = getMinValue();
        int maxValue = getMaxValue();
        boolean valueRangeDefined = false;
        int value = -1;
        boolean valueDefined = false;
        String familyName = null;
        int style = Typeface.NORMAL;
        boolean textStyleDefined = false;
        for (int i = 0, count = a.getIndexCount(); i < count; i++) {
            int attr = a.getIndex(i);
            if (attr == R.styleable.Slider_sl_discreteMode)
                mDiscreteMode = a.getBoolean(attr, false);
            else if (attr == R.styleable.Slider_sl_trackSize)
                mTrackSize = a.getDimensionPixelSize(attr, 0);
            else if (attr == R.styleable.Slider_sl_thumbBorderSize)
                mThumbBorderSize = a.getDimensionPixelSize(attr, 0);
            else if (attr == R.styleable.Slider_sl_thumbRadius)
                mThumbRadius = a.getDimensionPixelSize(attr, 0);
            else if (attr == R.styleable.Slider_sl_thumbFocusRadius)
                mThumbFocusRadius = a.getDimensionPixelSize(attr, 0);
            else if (attr == R.styleable.Slider_sl_travelAnimDuration) {
                mTravelAnimationDuration = a.getInteger(attr, 0);
                mTransformAnimationDuration = mTravelAnimationDuration;
            } else if (attr == R.styleable.Slider_sl_alwaysFillThumb) {
                mAlwaysFillThumb = a.getBoolean(R.styleable.Slider_sl_alwaysFillThumb, false);
            } else if (attr == R.styleable.Slider_sl_interpolator) {
                int resId = a.getResourceId(R.styleable.Slider_sl_interpolator, 0);
                mInterpolator = AnimationUtils.loadInterpolator(context, resId);
            } else if (attr == R.styleable.Slider_android_gravity)
                mGravity = a.getInteger(attr, 0);
            else if (attr == R.styleable.Slider_sl_minValue) {
                minValue = a.getInteger(attr, 0);
                valueRangeDefined = true;
            } else if (attr == R.styleable.Slider_sl_maxValue) {
                maxValue = a.getInteger(attr, 0);
                valueRangeDefined = true;
            } else if (attr == R.styleable.Slider_sl_stepValue)
                mStepValue = a.getInteger(attr, 0);
            else if (attr == R.styleable.Slider_sl_value) {
                value = a.getInteger(attr, 0);
                valueDefined = true;
            } else if (attr == R.styleable.Slider_sl_fontFamily) {
                familyName = a.getString(attr);
                textStyleDefined = true;
            } else if (attr == R.styleable.Slider_sl_textStyle) {
                style = a.getInteger(attr, 0);
                textStyleDefined = true;
            } else if (attr == R.styleable.Slider_sl_textSize)
                mTextSize = a.getDimensionPixelSize(attr, 0);
            else if (attr == R.styleable.Slider_android_enabled)
                setEnabled(a.getBoolean(attr, true));
            else if (attr == R.styleable.Slider_sl_baselineOffset)
                mBaselineOffset = a.getDimensionPixelOffset(attr, 0);
        }

        a.recycle();

        if (mTrackSize < 0)
            mTrackSize = ThemeUtil.dpToPx(context, 2);

        if (mThumbBorderSize < 0)
            mThumbBorderSize = ThemeUtil.dpToPx(context, 2);

        if (mThumbRadius < 0)
            mThumbRadius = ThemeUtil.dpToPx(context, 10);

        if (mThumbFocusRadius < 0)
            mThumbFocusRadius = ThemeUtil.dpToPx(context, 14);

        if (mTravelAnimationDuration < 0) {
            mTravelAnimationDuration = context.getResources().getInteger(android.R.integer.config_mediumAnimTime);
            mTransformAnimationDuration = mTravelAnimationDuration;
        }

        if (mInterpolator == null)
            mInterpolator = new DecelerateInterpolator();

        if (valueRangeDefined)
            setValueRange(minValue, maxValue, false);

        if (valueDefined)
            setValue(value, false);
        else if (mThumbPosition < 0)
            setValue(mMinValue, false);

        if (textStyleDefined)
            mTypeface = TypefaceUtil.load(context, familyName, style);

        if (mTextSize < 0)
            mTextSize = context.getResources().getDimensionPixelOffset(R.dimen.abc_text_size_small_material);

        mPaint.setTextSize(mTextSize);
        mPaint.setTextAlign(Paint.Align.CENTER);
        mPaint.setTypeface(mTypeface);

        invalidate();
    }

    @Override
    public void onThemeChanged(ThemeManager.OnThemeChangedEvent event) {
        int style = ThemeManager.getInstance().getCurrentStyle(mStyleId);
        if (mCurrentStyle != style) {
            mCurrentStyle = style;
            applyStyle(mCurrentStyle);
        }
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        if (mStyleId != 0) {
            ThemeManager.getInstance().registerOnThemeChangedListener(this);
            onThemeChanged(null);
        }
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        RippleManager.cancelRipple(this);
        if (mStyleId != 0)
            ThemeManager.getInstance().unregisterOnThemeChangedListener(this);
    }

    /**
     * @return The minimum selectable value.
     */
    public int getMinValue() {
        return mMinValue;
    }

    /**
     * @return The maximum selectable value.
     */
    public int getMaxValue() {
        return mMaxValue;
    }

    /**
     * Set the randge of selectable value.
     *
     * @param min       The minimum selectable value.
     * @param max       The maximum selectable value.
     * @param animation Indicate that should show animation when thumb's current position changed.
     */
    public void setValueRange(int min, int max, boolean animation) {
        if (max < min || (min == mMinValue && max == mMaxValue))
            return;

        float oldValue = getExactValue();
        mMinValue = min;
        mMaxValue = max;

        setValue(oldValue, animation);
    }

    /**
     * @return The exact selected value.
     */
    public float getExactValue() {
        return (mMaxValue - mMinValue) * getPosition() + mMinValue;
    }

    /**
     * @return The current position of thumb in [0..1] range.
     */
    public float getPosition() {
        return mThumbMoveAnimator.isRunning() ? mThumbMoveAnimator.getPosition() : mThumbPosition;
    }

    /**
     * Set current position of thumb.
     *
     * @param pos       The position in [0..1] range.
     * @param animation Indicate that should show animation when change thumb's position.
     */
    public void setPosition(float pos, boolean animation) {
        setPosition(pos, animation, animation, false);
    }

    private void setPosition(float pos, boolean moveAnimation, boolean transformAnimation, boolean fromUser) {

        if (!moveAnimation || !mThumbMoveAnimator.startAnimation(pos)) {
            mThumbPosition = pos;

            if (transformAnimation) {
                if (!mIsDragging)
                    mThumbRadiusAnimator.startAnimation(mThumbRadius);
                mThumbStrokeAnimator.startAnimation(pos == 0 ? 0 : 1);
            } else {
                mThumbCurrentRadius = mThumbRadius;
                mThumbFillPercent = (mAlwaysFillThumb || mThumbPosition != 0) ? 1 : 0;
                invalidate();
            }
        }
    }

    /**
     * Set the selected value of this Slider.
     *
     * @param value     The selected value.
     * @param animation Indicate that should show animation when change thumb's position.
     */
    public void setValue(float value, boolean animation) {
        value = Math.min(mMaxValue, Math.max(value, mMinValue));
        setPosition((value - mMinValue) / (mMaxValue - mMinValue), animation);
    }

    @Override
    public void setBackgroundDrawable(Drawable drawable) {
        Drawable background = getBackground();
        if (background instanceof RippleDrawable && !(drawable instanceof RippleDrawable))
            ((RippleDrawable) background).setBackgroundDrawable(drawable);
        else
            super.setBackgroundDrawable(drawable);
    }

    protected RippleManager getRippleManager() {
        if (mRippleManager == null) {
            synchronized (RippleManager.class) {
                if (mRippleManager == null)
                    mRippleManager = new RippleManager();
            }
        }

        return mRippleManager;
    }

    @Override
    public void setOnClickListener(OnClickListener l) {
        RippleManager rippleManager = getRippleManager();
        if (l == rippleManager)
            super.setOnClickListener(l);
        else {
            rippleManager.setOnClickListener(l);
            setOnClickListener(rippleManager);
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);

        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        switch (widthMode) {
            case MeasureSpec.UNSPECIFIED:
                widthSize = getSuggestedMinimumWidth();
                break;
            case MeasureSpec.AT_MOST:
                widthSize = Math.min(widthSize, getSuggestedMinimumWidth());
                break;
        }

        switch (heightMode) {
            case MeasureSpec.UNSPECIFIED:
                heightSize = getSuggestedMinimumHeight();
                break;
            case MeasureSpec.AT_MOST:
                heightSize = Math.min(heightSize, getSuggestedMinimumHeight());
                break;
        }

        setMeasuredDimension(widthSize, heightSize);
    }

    @Override
    public int getSuggestedMinimumWidth() {
        return (mDiscreteMode ? (int) (mThumbRadius * Math.sqrt(2)) : mThumbFocusRadius) * 4 + getPaddingLeft() + getPaddingRight();
    }

    @Override
    public int getSuggestedMinimumHeight() {
        return (mDiscreteMode ? (int) (mThumbRadius * (4 + Math.sqrt(2))) : mThumbFocusRadius * 2) + getPaddingTop() + getPaddingBottom();
    }

    @Override
    public void onRtlPropertiesChanged(int layoutDirection) {
        boolean rtl = layoutDirection == LAYOUT_DIRECTION_RTL;
        if (mIsRtl != rtl) {
            mIsRtl = rtl;
            invalidate();
        }
    }

    @Override
    public int getBaseline() {
        int align = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
        int baseline;

        if (mDiscreteMode) {
            int fullHeight = (int) (mThumbRadius * (4 + Math.sqrt(2)));
            int height = mThumbRadius * 2;
            switch (align) {
                case Gravity.TOP:
                    baseline = Math.max(getPaddingTop(), fullHeight - height) + mThumbRadius;
                    break;
                case Gravity.BOTTOM:
                    baseline = getMeasuredHeight() - getPaddingBottom();
                    break;
                default:
                    baseline = Math.round(Math.max((getMeasuredHeight() - height) / 2f, fullHeight - height) + mThumbRadius);
                    break;
            }
        } else {
            int height = mThumbFocusRadius * 2;
            switch (align) {
                case Gravity.TOP:
                    baseline = getPaddingTop() + mThumbFocusRadius;
                    break;
                case Gravity.BOTTOM:
                    baseline = getMeasuredHeight() - getPaddingBottom();
                    break;
                default:
                    baseline = Math.round((getMeasuredHeight() - height) / 2f + mThumbFocusRadius);
                    break;
            }
        }

        return baseline + mBaselineOffset;
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        mDrawRect.left = getPaddingLeft() + mThumbRadius;
        mDrawRect.right = w - getPaddingRight() - mThumbRadius;
        mBackground = BitmapUtils.resizeBitmap(mBackground, (int) mDrawRect.width());
        mPickColor = mBackground.getPixel(1, mBackground.getHeight() / 2);

        int align = mGravity & Gravity.VERTICAL_GRAVITY_MASK;

        if (mDiscreteMode) {
            int fullHeight = (int) (mThumbRadius * (4 + Math.sqrt(2)));
            int height = mThumbRadius * 2;
            switch (align) {
                case Gravity.TOP:
                    mDrawRect.top = Math.max(getPaddingTop(), fullHeight - height);
                    mDrawRect.bottom = mDrawRect.top + height;
                    break;
                case Gravity.BOTTOM:
                    mDrawRect.bottom = h - getPaddingBottom();
                    mDrawRect.top = mDrawRect.bottom - height;
                    break;
                default:
                    mDrawRect.top = Math.max((h - height) / 2f, fullHeight - height);
                    mDrawRect.bottom = mDrawRect.top + height;
                    break;
            }
        } else {
            int height = mThumbFocusRadius * 2;
            switch (align) {
                case Gravity.TOP:
                    mDrawRect.top = getPaddingTop();
                    mDrawRect.bottom = mDrawRect.top + height;
                    break;
                case Gravity.BOTTOM:
                    mDrawRect.bottom = h - getPaddingBottom();
                    mDrawRect.top = mDrawRect.bottom - height;
                    break;
                default:
                    mDrawRect.top = (h - height) / 2f;
                    mDrawRect.bottom = mDrawRect.top + height;
                    break;
            }
        }
    }

    private boolean isThumbHit(float x, float y, float radius) {
        float cx = mDrawRect.width() * mThumbPosition + mDrawRect.left;
        float cy = mDrawRect.centerY();

        return x >= cx - radius && x <= cx + radius && y >= cy - radius && y < cy + radius;
    }

    private double distance(float x1, float y1, float x2, float y2) {
        return Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2));
    }

    private float correctPosition(float position) {
        if (!mDiscreteMode)
            return position;

        int totalOffset = mMaxValue - mMinValue;
        int valueOffset = Math.round(totalOffset * position);
        int stepOffset = valueOffset / mStepValue;
        int lowerValue = stepOffset * mStepValue;
        int higherValue = Math.min(totalOffset, (stepOffset + 1) * mStepValue);

        if (valueOffset - lowerValue < higherValue - valueOffset)
            position = lowerValue / (float) totalOffset;
        else
            position = higherValue / (float) totalOffset;

        return position;
    }

    @Override
    public boolean onTouchEvent(@NonNull MotionEvent event) {
        super.onTouchEvent(event);
        getRippleManager().onTouchEvent(this, event);

        if (!isEnabled())
            return false;

        float x = event.getX();
        float y = event.getY();
        if (mIsRtl)
            x = 2 * mDrawRect.centerX() - x;

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mIsDragging = isThumbHit(x, y, mThumbRadius) && !mThumbMoveAnimator.isRunning();
                mMemoPoint.set(x, y);
                if (mIsDragging) {
                    mThumbRadiusAnimator.startAnimation(mDiscreteMode ? 0 : mThumbFocusRadius);

                    if (getParent() != null)
                        getParent().requestDisallowInterceptTouchEvent(true);
                }
                break;
            case MotionEvent.ACTION_MOVE:
                if (mIsDragging) {
                    if (mDiscreteMode) {
                        float position = correctPosition(Math.min(1f, Math.max(0f, (x - mDrawRect.left) / mDrawRect.width())));
                        setPosition(position, true, true, true);
                    } else {
                        float offset = (x - mMemoPoint.x) / mDrawRect.width();
                        float position = Math.min(1f, Math.max(0f, mThumbPosition + offset));
                        setPosition(position, false, true, true);
                        mMemoPoint.x = x;
                        invalidate();
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
                if (mIsDragging) {
                    mIsDragging = false;
                    setPosition(getPosition(), true, true, true);

                    if (getParent() != null)
                        getParent().requestDisallowInterceptTouchEvent(false);
                } else if (distance(mMemoPoint.x, mMemoPoint.y, x, y) <= mTouchSlop) {
                    float position = correctPosition(Math.min(1f, Math.max(0f, (x - mDrawRect.left) / mDrawRect.width())));
                    setPosition(position, true, true, true);
                }
                break;
            case MotionEvent.ACTION_CANCEL:
                if (mIsDragging) {
                    mIsDragging = false;
                    setPosition(getPosition(), true, true, true);

                    if (getParent() != null)
                        getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
        }

        return true;
    }

    private Path getMarkPath(Path path, float cx, float cy, float radius, float factor) {
        if (path == null)
            path = new Path();
        else
            path.reset();

        float x1 = cx - radius;
        float y1 = cy;
        float x2 = cx + radius;
        float y2 = cy;
        float x3 = cx;
        float y3 = cy + radius;

        float nCx = cx;
        float nCy = cy - radius * factor;

        // calculate first arc
        float angle = (float) (Math.atan2(y2 - nCy, x2 - nCx) * 180 / Math.PI);
        float nRadius = (float) distance(nCx, nCy, x1, y1);
        mTempRect.set(nCx - nRadius, nCy - nRadius, nCx + nRadius, nCy + nRadius);
        path.moveTo(x1, y1);
        path.arcTo(mTempRect, 180 - angle, 180 + angle * 2);

        if (factor > 0.9f)
            path.lineTo(x3, y3);
        else {
            // find center point for second arc
            float x4 = (x2 + x3) / 2;
            float y4 = (y2 + y3) / 2;

            double d1 = distance(x2, y2, x4, y4);
            double d2 = d1 / Math.tan(Math.PI * (1f - factor) / 4);

            nCx = (float) (x4 - Math.cos(Math.PI / 4) * d2);
            nCy = (float) (y4 - Math.sin(Math.PI / 4) * d2);

            // calculate second arc
            angle = (float) (Math.atan2(y2 - nCy, x2 - nCx) * 180 / Math.PI);
            float angle2 = (float) (Math.atan2(y3 - nCy, x3 - nCx) * 180 / Math.PI);
            nRadius = (float) distance(nCx, nCy, x2, y2);
            mTempRect.set(nCx - nRadius, nCy - nRadius, nCx + nRadius, nCy + nRadius);
            path.arcTo(mTempRect, angle, angle2 - angle);

            // calculate third arc
            nCx = cx * 2 - nCx;
            angle = (float) (Math.atan2(y3 - nCy, x3 - nCx) * 180 / Math.PI);
            angle2 = (float) (Math.atan2(y1 - nCy, x1 - nCx) * 180 / Math.PI);
            mTempRect.set(nCx - nRadius, nCy - nRadius, nCx + nRadius, nCy + nRadius);
            path.arcTo(mTempRect, angle + (float) Math.PI / 4, angle2 - angle);
        }

        path.close();

        return path;
    }

    public int getPickColor() {
        return mPickColor;
    }

    @Override
    public void draw(@NonNull Canvas canvas) {
        super.draw(canvas);
        float x = mDrawRect.width() * mThumbPosition + mDrawRect.left;
        if (mIsRtl)
            x = 2 * mDrawRect.centerX() - x;
        float y = mDrawRect.centerY();
        mPaint.setStyle(Paint.Style.FILL);
        canvas.drawBitmap(mBackground, mDrawRect.left, y - mBackground.getHeight() / 2, mPaint);

        int colorX = (int) x - mThumbRadius * 2;
        if (colorX <= 0) {
            colorX = 1;
        } else if (colorX >= mBackground.getWidth()) {
            colorX = mBackground.getWidth() - 1;
        }
        int pixel = mBackground.getPixel(colorX, mBackground.getHeight() / 2);
        mPickColor = pixel;
        mPaint.setColor(pixel);
        if (mDiscreteMode) {
            float factor = 1f - mThumbCurrentRadius / mThumbRadius;

            if (factor > 0) {
                mMarkPath = getMarkPath(mMarkPath, x, y, mThumbRadius, factor);
                mPaint.setStyle(Paint.Style.FILL);
                int saveCount = canvas.save();
                canvas.translate(0, -mThumbRadius * 2 * factor);
                canvas.drawPath(mMarkPath, mPaint);
                canvas.restoreToCount(saveCount);
            }

            float radius = isEnabled() ? mThumbCurrentRadius : mThumbCurrentRadius - mThumbBorderSize;
            if (radius > 0) {
                canvas.drawCircle(x, y, radius, mPaint);
            }
        } else {
            float radius = isEnabled() ? mThumbCurrentRadius : mThumbCurrentRadius - mThumbBorderSize;
            if (mThumbFillPercent == 1)
                mPaint.setStyle(Paint.Style.FILL);
            else {
                float strokeWidth = (radius - mThumbBorderSize) * mThumbFillPercent + mThumbBorderSize;
                radius = radius - strokeWidth / 2f;
                mPaint.setStyle(Paint.Style.STROKE);
                mPaint.setStrokeWidth(strokeWidth);
            }
            canvas.drawCircle(x, y, radius, mPaint);
        }
    }

    @Override
    protected Parcelable onSaveInstanceState() {
        Parcelable superState = super.onSaveInstanceState();

        SavedState ss = new SavedState(superState);

        ss.position = getPosition();
        return ss;
    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        SavedState ss = (SavedState) state;

        super.onRestoreInstanceState(ss.getSuperState());
        setPosition(ss.position, false);
        requestLayout();
    }

    public interface OnColorChangeListener {
        void changeColor(int color, float pos);
    }

    static class SavedState extends BaseSavedState {
        public static final Creator<SavedState> CREATOR
                = new Creator<SavedState>() {
            public SavedState createFromParcel(Parcel in) {
                return new SavedState(in);
            }

            public SavedState[] newArray(int size) {
                return new SavedState[size];
            }
        };
        float position;

        /**
         * Constructor called from {@link Slider#onSaveInstanceState()}
         */
        SavedState(Parcelable superState) {
            super(superState);
        }

        /**
         * Constructor called from {@link #CREATOR}
         */
        private SavedState(Parcel in) {
            super(in);
            position = in.readFloat();
        }

        @Override
        public void writeToParcel(@NonNull Parcel out, int flags) {
            super.writeToParcel(out, flags);
            out.writeFloat(position);
        }

        @Override
        public String toString() {
            return "Slider.SavedState{"
                    + Integer.toHexString(System.identityHashCode(this))
                    + " pos=" + position + "}";
        }
    }

    class ThumbRadiusAnimator implements Runnable {

        boolean mRunning = false;
        long mStartTime;
        float mStartRadius;
        int mRadius;

        public void resetAnimation() {
            mStartTime = SystemClock.uptimeMillis();
            mStartRadius = mThumbCurrentRadius;
        }

        public boolean startAnimation(int radius) {
            if (mThumbCurrentRadius == radius)
                return false;

            mRadius = radius;

            if (getHandler() != null) {
                resetAnimation();
                mRunning = true;
                getHandler().postAtTime(this, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION);
                invalidate();
                return true;
            } else {
                mThumbCurrentRadius = mRadius;
                invalidate();
                return false;
            }
        }

        public void stopAnimation() {
            mRunning = false;
            mThumbCurrentRadius = mRadius;
            if (getHandler() != null)
                getHandler().removeCallbacks(this);
            invalidate();
        }

        @Override
        public void run() {
            long curTime = SystemClock.uptimeMillis();
            float progress = Math.min(1f, (float) (curTime - mStartTime) / mTransformAnimationDuration);
            float value = mInterpolator.getInterpolation(progress);

            mThumbCurrentRadius = (mRadius - mStartRadius) * value + mStartRadius;

            if (progress == 1f)
                stopAnimation();

            if (mRunning) {
                if (getHandler() != null)
                    getHandler().postAtTime(this, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION);
                else
                    stopAnimation();
            }

            invalidate();
        }

    }

    class ThumbStrokeAnimator implements Runnable {

        boolean mRunning = false;
        long mStartTime;
        float mStartFillPercent;
        int mFillPercent;

        public void resetAnimation() {
            mStartTime = SystemClock.uptimeMillis();
            mStartFillPercent = mThumbFillPercent;
        }

        public boolean startAnimation(int fillPercent) {
            if (mThumbFillPercent == fillPercent)
                return false;

            mFillPercent = fillPercent;

            if (getHandler() != null) {
                resetAnimation();
                mRunning = true;
                getHandler().postAtTime(this, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION);
                invalidate();
                return true;
            } else {
                mThumbFillPercent = mAlwaysFillThumb ? 1 : mFillPercent;
                invalidate();
                return false;
            }
        }

        public void stopAnimation() {
            mRunning = false;
            mThumbFillPercent = mAlwaysFillThumb ? 1 : mFillPercent;
            if (getHandler() != null)
                getHandler().removeCallbacks(this);
            if (mOnColorChangeListener != null) {
                mOnColorChangeListener.changeColor(mPickColor, mThumbPosition);
            }
            invalidate();
        }

        @Override
        public void run() {
            long curTime = SystemClock.uptimeMillis();
            float progress = Math.min(1f, (float) (curTime - mStartTime) / mTransformAnimationDuration);
            float value = mInterpolator.getInterpolation(progress);

            mThumbFillPercent = mAlwaysFillThumb ? 1 : ((mFillPercent - mStartFillPercent) * value + mStartFillPercent);

            if (progress == 1f)
                stopAnimation();

            if (mRunning) {
                if (getHandler() != null)
                    getHandler().postAtTime(this, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION);
                else
                    stopAnimation();
            }

            invalidate();
        }

    }

    class ThumbMoveAnimator implements Runnable {

        boolean mRunning = false;
        long mStartTime;
        float mStartFillPercent;
        float mStartRadius;
        float mStartPosition;
        float mPosition;
        float mFillPercent;
        int mDuration;

        public boolean isRunning() {
            return mRunning;
        }

        public float getPosition() {
            return mPosition;
        }

        public void resetAnimation() {
            mStartTime = SystemClock.uptimeMillis();
            mStartPosition = mThumbPosition;
            mStartFillPercent = mThumbFillPercent;
            mStartRadius = mThumbCurrentRadius;
            mFillPercent = mPosition == 0 ? 0 : 1;
            mDuration = mDiscreteMode && !mIsDragging ? mTransformAnimationDuration * 2 + mTravelAnimationDuration : mTravelAnimationDuration;
        }

        public boolean startAnimation(float position) {
            if (mThumbPosition == position)
                return false;

            mPosition = position;

            if (getHandler() != null) {
                resetAnimation();
                mRunning = true;
                getHandler().postAtTime(this, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION);
                invalidate();
                return true;
            } else {
                mThumbPosition = position;
                invalidate();
                return false;
            }
        }

        public void stopAnimation() {
            mRunning = false;
            mThumbCurrentRadius = mDiscreteMode && mIsDragging ? 0 : mThumbRadius;
            mThumbFillPercent = mAlwaysFillThumb ? 1 : mFillPercent;
            mThumbPosition = mPosition;
            if (getHandler() != null)
                getHandler().removeCallbacks(this);
            if (mOnColorChangeListener != null) {
                mOnColorChangeListener.changeColor(mPickColor, mThumbPosition);
            }
            invalidate();
        }

        @Override
        public void run() {
            long curTime = SystemClock.uptimeMillis();
            float progress = Math.min(1f, (float) (curTime - mStartTime) / mDuration);
            float value = mInterpolator.getInterpolation(progress);

            if (mDiscreteMode) {
                if (mIsDragging) {
                    mThumbPosition = (mPosition - mStartPosition) * value + mStartPosition;
                    mThumbFillPercent = mAlwaysFillThumb ? 1 : ((mFillPercent - mStartFillPercent) * value + mStartFillPercent);
                } else {
                    float p1 = (float) mTravelAnimationDuration / mDuration;
                    float p2 = (float) (mTravelAnimationDuration + mTransformAnimationDuration) / mDuration;
                    if (progress < p1) {
                        value = mInterpolator.getInterpolation(progress / p1);
                        mThumbCurrentRadius = mStartRadius * (1f - value);
                        mThumbPosition = (mPosition - mStartPosition) * value + mStartPosition;
                        mThumbFillPercent = mAlwaysFillThumb ? 1 : ((mFillPercent - mStartFillPercent) * value + mStartFillPercent);
                    } else if (progress > p2) {
                        mThumbCurrentRadius = mThumbRadius * (progress - p2) / (1 - p2);
                    }
                }
            } else {
                mThumbPosition = (mPosition - mStartPosition) * value + mStartPosition;
                mThumbFillPercent = mAlwaysFillThumb ? 1 : ((mFillPercent - mStartFillPercent) * value + mStartFillPercent);

                if (progress < 0.2)
                    mThumbCurrentRadius = Math.max(mThumbRadius + mThumbBorderSize * progress * 5, mThumbCurrentRadius);
                else if (progress >= 0.8)
                    mThumbCurrentRadius = mThumbRadius + mThumbBorderSize * (5f - progress * 5);
            }


            if (progress == 1f)
                stopAnimation();

            if (mRunning) {
                if (getHandler() != null)
                    getHandler().postAtTime(this, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION);
                else
                    stopAnimation();
            }

            invalidate();
        }

    }
}
